在上一篇文章中,我們提到延遲綁定時 dl-resolve(link_map, index) 將實際地址填入 .got.plt 的流程。
現在我們將繼續說明具體要如何操作此攻擊手法。此篇文章架構如下:
dl-resolve(link_map, index)
dl-resolve(link_map, index)上篇文章的結尾有提到整個程式的流程如下:
為了更清楚了解攻擊流程,接下來我們將逐步搭配程式來詳細說明每個步驟的細節。

我們之前透過 readelf -d 看過 ELF 檔案中的 .dynamic Section。實際上我們現在看到的內容在 ELF 檔中是以名為 Elf64_Dyn 的 Struct 的形式儲存,如下:
struct Elf64_Dyn {
Elf64_Sxword d_tag
union {
Elf64_Xword d_val
Elf64_Addr d_ptr
} d_un
};
readelf 的內容對應下來如下
所以第一步 dl-resolve 函數會使用 link_map 找到 Elf64_Dyn Struct 中 d_tag 為 0x5、0x6、0x17 的 d_ptr,來獲取這三個表(等同於上圖黃色的 Section)的位址。
註:使用跟上一篇相同的例子說明,即需要延遲綁定的函數為
printf()。
第二個參數 index,代表 printf() 的重定位表實際上是重定位表第幾組,例如:
index 當初是被設為 0,因此他現在是重定位表中的第一組。
如果呼叫 printf 之後又呼叫另一個外部函數,例如 scanf,就會像下面這樣:
index 被設為 1,且可以看到位於重定位表的第二組。
所以現在我們得知真正的重定位表位址為:JMPPEL(0x17) + index*一組的大小,我們稱呼它為 reloc。
我們來看一下 reloc 的內容,重定位表的內容在 ELF 檔中以名為 Elf64_Rel 的 Struct 的形式儲存,如下:
struct Elf64_Rel {
Elf64_Addr r_offset;
Elf64_Xword r_info;
};
其中 reloc -> r_offset 為 .got.plt 的位址,而 .got.plt 的位址上會儲存最後拿到的記憶體實際位址。reloc -> r_info 的前 8 bytes 用於儲存符號表的索引值,後 8 bytes 代表類型。可以透過 readelf -r 查看,如下:
可以看到 Offset 即 reloc -> r_offset 就是.got.plt 的位址,而 Info 即為 reloc -> r_info,前 8 bytes 為 0x00000002(前面補上4個0)、後 8 為 0x00000007,代表 R_X86_64_JUMP_SLO 類型。
我們可以看到上面的資訊表示 printf() 的符號表位於符號表中 Index 為 2 的位置,如下:
從剛剛的 reloc 已經找到 printf() 的符號表實際上位於索引值 2 號,我們稱呼這個實際上的符號表為 sym。
來看一下 sym 的內容,它以名為 Elf64_Sym 的 Struct 的形式儲存
struct Elf64_Sym {
Elf64_Word st_name; // Symbol name (index into string table)
unsigned char st_info; // Symbol's type and binding attributes
unsigned char st_other; // Must be zero; reserved
Elf64_Half st_shndx; // Which section (header tbl index) it's defined in
Elf64_Addr st_value; // Value or address associated with the symbol
Elf64_Xword st_size; // Size of the symbol
}
其中 sym -> st_name 儲存字串表的位移,也就是說字串表的開頭 STRTAB(0x5) + sym -> st_name = "printf" 的位址。
最後 dl-resolve 會使用這個字串去尋找實際位址,然後儲存至 reloc -> r_offset 即 .got.plt 的位址。
依據上述的流程,可以看出我們只要篡改字串表的內容,例如直接把 printf 改為 system,當程式在延遲綁定 printf() 時,實際上會將 system() 的位址寫入到 .got.plt。這樣,雖然原始的指令是呼叫 printf(),但由於綁定過程中相應位置的字串表被篡改,程式會執行 system(),從而劫持控制流程並執行任意命令。
所以既然是藉由篡改內容而造成的攻擊,那就讓那部份的記憶體位址不能被寫就好了,所以就有了 RELRO(ReLocation Read-Only) 的機制,除了 .got.plt 設為可寫,讓實際地址可以寫上以外,其他所有相關 Sections,像是 .dynamic、.got 都只可讀。這樣攻擊者就不能篡改字串了。
那不能改字串表就不能攻擊了嗎?只要能控制輸入,就可能發動攻擊,還記得我們提到的第二個參數 index 嗎?
如果我們控制 index,讓程式認為重定位表位於一個可寫區段,並且我們在此可寫區段寫一個假的 reloc、sym 以及字串表,是不是就能達成攻擊了呢?
所以整個攻擊思路如下:
index,使得 reloc 位於可寫入位址。reloc,使得 reloc -> r_info 能自己控制,因此能於此創建假的 sym。sym -> st_name 寫入需要的字串,如 system。如此一來,我們就能成功獲取控制權了。
從上面可以發現,此攻擊的根本在於延遲綁定的機制,因此 Full RELRO 保護機制是直接取消延遲綁定這個功能,程式一開始就直接全部綁定了。
因此阻絕了所有利用延遲綁定功能的攻擊,例如 ret2dlresolve。